package bitch;
//The Server handles the TCP Connections.

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.SocketException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;
import java.util.Hashtable;

import javax.swing.JFrame;
import javax.swing.JTextField;

import constants.PROTOCOL;
import constants.STATUS;
import constants.TYPE;


public class Server
	{	
	private String str;
	private boolean isRunning;
	private ServerSocket myService;
	private ServerParser serverParser;
	private Accepter accepter;
	private JTextField textField;
	private Database database;
	
	public Vector<TCPConnection> tCPConnections;
	public Hashtable<Integer,TCPConnection> connectedPersons;
	public Hashtable<Integer,Access> accesses;
	
	public static void main(String[]a)
		{
		new Server();
		
		//new GUI();
		}
	//Instantiate server(this also instantiates all other necessary objects)
	public Server()
		{
		isRunning=true;
		JFrame frame = new JFrame();
		frame.addWindowListener(new WindowAdapter()
		    {
		        public void windowClosing(WindowEvent e)
		        {
		        	dispose();
		            System.exit(0);
		        }
		    });
		textField = new JTextField("CALENDAR SYSTEM 5000 SERVER");
		frame.add(textField);
		frame.pack();
		frame.setVisible(true);
		database = new Database();
		serverParser = new ServerParser(this,database);
		
		
		str="";
		tCPConnections=new Vector<TCPConnection>();
		connectedPersons=new Hashtable<Integer,TCPConnection>();
		accesses=new Hashtable<Integer,Access>();
		
		
		try
			{myService = new ServerSocket(1337);}
		catch (IOException e)
			{e.printStackTrace();}
		
		accepter = new Accepter();
		accepter.start();
		}
	//Parse a string
	public synchronized void parse(String str,TCPConnection source)
		{
		serverParser.parse(str,source);
		}
	
	public synchronized void setText(String s)
		{
		str=s;
		textField.setText(s);
		}
	public synchronized String getText()
		{
		return str;
		}
	//Broadcast a message to all clients
	public void broadcast(String str)
		{
		for (TCPConnection c : tCPConnections)
			{
			c.send(str);
			}
		}
	//Handle when an appointment is cancelled
	public void delAppointment(int ownerID,int appointmentID,String reason,String receivedString)
		{
		database.update("UPDATE Appointment SET isCancelled=1 WHERE ownerID="+ownerID+" AND appointmentID="+appointmentID);
		//Get the timestamps for the cancelled appointment
		Timestamp fromTime = null;
		Timestamp toTime = null;
		ResultSet rs=database.query("SELECT startTime,endTime FROM Appointment WHERE ownerID="+ownerID+" AND appointmentID="+appointmentID+"");
			try
				{
				if(rs.next())
					{
					fromTime	= rs.getTimestamp(1+0);
					toTime	= rs.getTimestamp(1+1);
					}
				else
					System.err.println("Trying to cancel a nonexisting appointment!");
				rs.close();
				}
			catch (SQLException e)
				{e.printStackTrace();}
		
		//Notify all the participants except owner.
		rs=database.getParticipants(ownerID,appointmentID,false);
		try
			{
			while (rs.next())
				{
				int participant = rs.getInt(1+0);
				if(participant!=ownerID)//Owner doesn't need a message.
					{
					//send the message
					sendMessage(TYPE.CANCELLED,participant,ownerID,appointmentID,-1,reason); 
					}
				}
			rs.close();
			}
		catch (SQLException e)
			{e.printStackTrace();}
		//Send the actual change to those who are accessing the appointment.
		//For all connected clients:
		Iterator<TCPConnection> it = connectedPersons.values().iterator();
		while (it.hasNext())
			{
			TCPConnection c = it.next();
			//If they are accessing the received appoinment or possibly the old one in some way
			if (isPersonAccessing(c.getPersonID(),ownerID,appointmentID,fromTime,toTime))
				{
				//notify them of the change
				c.send(receivedString);
				}
			}
		}
	//Handle when an appointment is created.
	public void setAppointment(int ownerID,int appointmentID,Timestamp fromTime,Timestamp toTime,String subject,String description,int roomID,String locationName,int changedFromID,int isCancelled,String receivedString,TCPConnection source)
		{
		boolean roomIsOK=true;
		if (roomID!=-1)
			{
			int exceptAppID=appointmentID;
			if(changedFromID!=-1)
				exceptAppID=changedFromID;
			if(!isRoomAvailable(roomID,fromTime,toTime,ownerID,exceptAppID))
				roomIsOK=false;
			}
		if (roomIsOK)
			{
			source.send("<"+PROTOCOL.APPOINTMENT_OK+"|1>");
			Timestamp oldFromTime = null;
			Timestamp oldToTime = null;
			//Create the appointment
			database.update("INSERT INTO Appointment VALUES ("+ownerID+","+appointmentID+",'"+fromTime+"','"+toTime+"','"+subject+"','"+description+"',"+isCancelled+",'"+locationName+"',"+roomID+",NULL)");
			//Set owner's participation
			database.update("INSERT INTO participation VALUES ("+ownerID+","+ownerID+","+appointmentID+","+STATUS.OWNER+")");
			//If this is an appointment change:
			if(changedFromID!=-1)
				{
				//get timestamps from old appointment for later use
				ResultSet rs=database.query("SELECT startTime,endTime FROM Appointment WHERE ownerID="+ownerID+" AND appointmentID="+changedFromID+"");
				try
					{
					if(rs.next())
						{
						oldFromTime	= rs.getTimestamp(1+0);
						oldToTime	= rs.getTimestamp(1+1);
						}
					else
						System.err.println("Trying to change a nonexisting appointment!");
					rs.close();
					}
				catch (SQLException e)
					{e.printStackTrace();}
				//Destroy the old appointment
				database.update("UPDATE Appointment SET isCancelled=1, changedToID="+appointmentID+" WHERE ownerID="+ownerID+" AND appointmentID="+changedFromID);
				//copy all old participation relations
				rs=database.getParticipants(ownerID,changedFromID,true);
				try
					{
					while (rs.next())
						{
						int oldParticipant = rs.getInt(1+0);
						if(oldParticipant!=ownerID)//Owner relation is already set above
							{
							//New participation type: not replied.
							database.update("INSERT INTO participation VALUES ("+oldParticipant+","+ownerID+","+appointmentID+","+STATUS.NOTREPLIED+")");
							//send change message
							sendMessage(TYPE.CHANGED,oldParticipant,ownerID,changedFromID,appointmentID,""); 
							}
						}
					rs.close();
					}
				catch (SQLException e)
					{e.printStackTrace();}
				}
			//Send the actual change to those who are accessing the appointment.
			//For all connected clients:
			Iterator<TCPConnection> it = connectedPersons.values().iterator();
			while (it.hasNext())
				{
				TCPConnection c = it.next();
				boolean ok = false;
				//If they are accessing the received appoinment or possibly the old one in some way
				if (isPersonAccessing(c.getPersonID(),ownerID,appointmentID,fromTime,toTime))
					ok=true;
				else if (changedFromID!=-1)
					{
					if(isPersonAccessing(c.getPersonID(),ownerID,changedFromID,oldFromTime,oldToTime))
						ok=true;
					}
				if(ok)
					{
					//notify them of the change
					c.send(receivedString);
					}
				}
			}
		else
			{
			//The room wasn't available...<
			source.send("<"+PROTOCOL.APPOINTMENT_OK+"|0>");
			}
		}
	//Check is a Person is currently accessing an appointment
	//Either because it's in the calendar view they're looking at or because they're looking at it's detailed view.
	public boolean isPersonAccessing(int personID, int ownerID,int appointmentID,Timestamp fromTime,Timestamp toTime)
		{
		Access a = accesses.get(personID);
		if(a.hasSpecificAccess(ownerID,appointmentID))
			return true;
		if(a.getToTime()!=null && a.getFromTime()!=null)
			{
			if(fromTime.before(a.getToTime()) && toTime.after(a.getFromTime()))
				{
				ResultSet rs = database.getParticipants(ownerID,appointmentID,false);
				try
					{
					while (rs.next())
						{
						if(a.isAccessingPerson(rs.getInt(1+0)))
							{
							return true;
							}
						}
					rs.close();
					}
				catch (SQLException e)
					{e.printStackTrace();}
				}
			}
			
		return false;
		}
	public void accessRange(Timestamp start,Timestamp end,TCPConnection source)
		{
		//set access
		Access access = accesses.get(source.getPersonID());
		access.accessRange(start, end);
		int[] persons = access.getPersons();
		for(int i=0; i<persons.length; i++)
			{
			//get all appointments for participantID in the range
			int participantID=persons[i];
			System.out.println("Fetching appointments for "+participantID+" in range "+start+", "+end);
			ResultSet rs = database.query("SELECT * FROM Appointment as a,participation as p WHERE a.ownerID=p.ownerID AND a.appointmentID=p.appointmentID AND p.participantID="+participantID+" AND a.startTime<'"+end+"' AND a.endTime>'"+start+"'");
			try
				{
				while (rs.next())
					{
					//send the appointment
					source.send("<"+PROTOCOL.APPOINTMENT+'|'+rs.getInt(1+0)+'|'+rs.getInt(1+1)+'|'+rs.getTimestamp(1+2).getTime()+'|'+rs.getTimestamp(1+3).getTime()+'|'+rs.getString(1+4)+'|'+rs.getString(1+5)+'|'+rs.getInt(1+8)+'|'+rs.getString(1+7)+'|'+rs.getInt(1+9)+"|-1|"+rs.getInt(1+6)+">");			
					
					//Get participants
					ResultSet rs2=database.getParticipants(rs.getInt(1+0), rs.getInt(1+1), true);
					while (rs2.next())
						{//send attendances.
						source.send("<"+PROTOCOL.SET_ATTENDANCE+'|'+rs.getInt(1+0)+'|'+rs.getInt(1+1)+'|'+rs2.getInt(1+0)+'|'+rs2.getInt(1+1)+"|>");
						}
					rs2.close();
					}
				rs.close();
					}
			catch (SQLException e)
				{e.printStackTrace();}
			}
		//send ack
		source.send("<"+PROTOCOL.ACK_APPOINTMENTS+"|0>");
		}
	public void accessPerson(int participantID,TCPConnection source)
		{
		//set access
		Access access = accesses.get(source.getPersonID());
		access.accessPerson(participantID);
		Timestamp start = access.getFromTime();
		Timestamp end 	= access.getToTime();
		
		//get all appointments for participantID in the range
		ResultSet rs = database.query("SELECT * FROM Appointment as a,participation as p WHERE a.ownerID=p.ownerID AND a.appointmentID=p.appointmentID AND p.participantID="+participantID+" AND a.startTime<'"+end+"' AND a.endTime>'"+start+"'");
		try
			{
			while (rs.next())
				{
				//send the appointment
				source.send("<"+PROTOCOL.APPOINTMENT+'|'+rs.getInt(1+0)+'|'+rs.getInt(1+1)+'|'+rs.getTimestamp(1+2).getTime()+'|'+rs.getTimestamp(1+3).getTime()+'|'+rs.getString(1+4)+'|'+rs.getString(1+5)+'|'+rs.getInt(1+8)+'|'+rs.getString(1+7)+'|'+rs.getInt(1+9)+"|-1|"+rs.getInt(1+6)+">");			
				
				//Get participants
				ResultSet rs2=database.getParticipants(rs.getInt(1+0), rs.getInt(1+1), true);
				while (rs2.next())
					{//send attendances.
					source.send("<"+PROTOCOL.SET_ATTENDANCE+'|'+rs.getInt(1+0)+'|'+rs.getInt(1+1)+'|'+rs2.getInt(1+0)+'|'+rs2.getInt(1+1)+"|>");
					}
				rs2.close();
				}
			rs.close();
			}
		catch (SQLException e)
			{e.printStackTrace();}
		
		
		//send ack
		source.send("<"+PROTOCOL.ACK_APPOINTMENTS+"|0>");
		}
	public void setAttendance(int participantID,int ownerID,int appointmentID,int type,String reason)
		{
		try
			{
			if(type != STATUS.DELETE)//save it in the database
				{
				//If the relation does not exist
				ResultSet rs = database.query("SELECT type FROM participation WHERE participantID="+participantID+" AND ownerID="+ownerID+" AND appointmentID="+appointmentID);
				if(!rs.next())
					database.update("INSERT INTO participation VALUES("+participantID+","+ownerID+","+appointmentID+","+type+")");
				else 
					database.update("UPDATE participation SET type="+type+" WHERE participantID="+participantID+" AND ownerID="+ownerID+" AND appointmentID="+appointmentID);
				rs.close();
				}
			
			//Get participants(including eventual added and to-be deleted)
			ResultSet rs2 = database.getParticipants(ownerID, appointmentID, true);
			ArrayList<Integer> allParticipants = new ArrayList<Integer>();
			ArrayList<Integer> allStatuses = new ArrayList<Integer>();
			while (rs2.next())
				{
				allParticipants.add(rs2.getInt(1+0));
				allStatuses.add(rs2.getInt(1+1));
				}
			rs2.close();
			
			//Get the appointment
			ResultSet rs = database.query("SELECT * FROM Appointment WHERE ownerID="+ownerID+" AND appointmentID="+appointmentID+"");
			if (!rs.next()) System.err.println("ERROR: trying to set attendance for nonexisting appointment!");
			Timestamp startTime = rs.getTimestamp(1+2);
			Timestamp endTime = rs.getTimestamp(1+3);
			String subject=rs.getString(1+4);
			String description=rs.getString(1+5);
			int isCancelled = rs.getInt(1+6);
			String locationName = rs.getString(1+7);
			int roomReservationID = rs.getInt(1+8);
			int changedToID = rs.getInt(1+9);
		
			if (type==STATUS.NOTREPLIED)//Must send the appointment invitation Message
					{
					sendMessage(TYPE.INVITATION,participantID,ownerID,appointmentID,-1,reason);
					}
			if(type==STATUS.DELETE)
				{
				//update the database
				database.update("DELETE FROM participation WHERE participantID="+participantID+" AND ownerID="+ownerID+" AND appointmentID ="+appointmentID+"");
				//send message to tell em that ey were deleted
				sendMessage(TYPE.DELETED,participantID,ownerID,appointmentID,-1,reason);
				}
			//Apart from messages, we only have to send stuff to
			//those accessing any participant in this appointment in this timeframe.
			//we do not have to send all the data if
			//the user is already accessing an old participant in this timeframe
			//In addition, we do not have to send all data if this is a cancel/delete.
			
			//Get all accesses and iterate over them.
			Object[] allAccesses = accesses.values().toArray();
			for(int i=0; i<allAccesses.length; i++)
				{
				//Investigate to which degree we should send stuff to this recipient.
				Access access = (Access)allAccesses[i];
				int sendTo=0;
				//Only send if they're looking at this timeframe
				if (access.getFromTime().before(endTime) && access.getToTime().after(startTime))
					{
					if(access.isAccessingPerson(participantID))
						sendTo=2;
					for(int j=0; j<allParticipants.size(); j++)
						{
						int currentID		= allParticipants.get(j);
						int currentStatus	= allStatuses.get(j);
						if (currentStatus<=2 && currentID!=participantID)
							{
							if (access.isAccessingPerson(currentID))
								sendTo=1;
							}
						}
					}
				//Send stuff according to what we found.
				if(sendTo==2 && type<=2)
					{
					TCPConnection recipient = connectedPersons.get(access.getPersonID());
					//send everything.
					//	send appointment
					recipient.send("<"+PROTOCOL.APPOINTMENT+'|'+ownerID+'|'+appointmentID+'|'+startTime.getTime()+'|'+endTime.getTime()+'|'+subject+'|'+description+'|'+roomReservationID+'|'+locationName+'|'+changedToID+"|-1|"+isCancelled+">");			
					//send attendances.
					for(int k=0; k<allParticipants.size(); k++)
						{
						recipient.send("<"+PROTOCOL.SET_ATTENDANCE+'|'+ownerID+'|'+appointmentID+'|'+allParticipants.get(k)+'|'+allStatuses.get(k)+"|>");
						}
					}
				else if (sendTo>0)
					{
					TCPConnection recipient = connectedPersons.get(access.getPersonID());
					//send only the new relation.
					recipient.send("<"+PROTOCOL.SET_ATTENDANCE+'|'+ownerID+'|'+appointmentID+'|'+participantID+'|'+type+"|>");
					}
				}
			if (type==STATUS.LEFT)//send LEFT message to all participants
				{
				//Reset.
				for(int i=0; i<allParticipants.size(); i++)
					{
					//Send the message to all who haven't denied or left.
					if(allStatuses.get(i) <= 2)
						{
						int current = allParticipants.get(i);
						sendMessage(TYPE.LEFT,current,ownerID,appointmentID,participantID,reason);
						}
					}
				}
			if (type==STATUS.DENIED)//send message only to the owner
				sendMessage(TYPE.DENIED,ownerID,ownerID,appointmentID,participantID,reason);
			}
		catch (SQLException e)
			{e.printStackTrace();}

		}		
	public void getAppointment(int ownerID,int appointmentID,TCPConnection source)
		{
		//set access
		accesses.get(source.getPersonID()).accessSpecific(ownerID, appointmentID);
		
		ResultSet rs = database.query("SELECT * FROM Appointment WHERE ownerID="+ownerID+" AND appointmentID="+appointmentID+"");
		try
			{
			if (rs.next())
				{
				//	send appointment
				source.send("<"+PROTOCOL.APPOINTMENT+'|'+rs.getInt(1+0)+'|'+rs.getInt(1+1)+'|'+rs.getTimestamp(1+2).getTime()+'|'+rs.getTimestamp(1+3).getTime()+'|'+rs.getString(1+4)+'|'+rs.getString(1+5)+'|'+rs.getInt(1+8)+'|'+rs.getString(1+7)+'|'+rs.getInt(1+9)+"|-1|"+rs.getInt(1+6)+">");			
				rs.close();
				//Get participants
				ResultSet rs2=database.getParticipants(ownerID, appointmentID, true);
				while (rs2.next())
					{//send attendances.
					source.send("<"+PROTOCOL.SET_ATTENDANCE+'|'+ownerID+'|'+appointmentID+'|'+rs2.getInt(1+0)+'|'+rs2.getInt(1+1)+"|>");
					}
				rs2.close();
				}
			}
		catch (SQLException e)
			{e.printStackTrace();}
		//send ack
		source.send("<"+PROTOCOL.ACK_APPOINTMENTS+"|1>");
		}
	//Send a "Message" to a client and store it in the database
	public void sendMessage(int type,int toPersonID,int ownerPersonID,int appointmentID,int auxField,String optionalString)
		{
		int messageID=database.messageCounterUp(toPersonID);
		long now = System.currentTimeMillis();
		String personDenied="NULL";
		String newAppointment="NULL";
		if (type==TYPE.CHANGED)
			newAppointment=String.valueOf(auxField);
		if (type==TYPE.DENIED || type==TYPE.LEFT)
			personDenied=String.valueOf(auxField);
		System.out.println("Message type "+type+" stored for "+toPersonID);
		database.update("INSERT INTO Message VALUES ("+toPersonID+","+messageID+","+type+",0,'"+new Timestamp(now)+"','"+optionalString+"',"+personDenied+","+ownerPersonID+","+appointmentID+","+newAppointment+")");
		//Actually send the message if they are connected.
		if(connectedPersons.containsKey(toPersonID))
			connectedPersons.get(toPersonID).send("<"+PROTOCOL.MESSAGE+'|'+type+'|'+toPersonID+'|'+messageID+'|'+ownerPersonID+'|'+appointmentID+'|'+auxField+'|'+optionalString+'|'+now+">");
		}
	//Check availability and return the result to the source.
	public void checkAvailability(int roomID,Timestamp startTime,Timestamp endTime,int ownerID,int appointmentID,TCPConnection source)
		{
		if(isRoomAvailable(roomID,startTime,endTime,ownerID,appointmentID))
			source.send("<"+PROTOCOL.CHECK_ROOM+"|"+roomID+"|1|-1|-1|-1>");
		else
			source.send("<"+PROTOCOL.CHECK_ROOM+"|"+roomID+"|0|-1|-1|-1>");
		}
	public boolean isRoomAvailable(int roomID,Timestamp startTime,Timestamp endTime,int exceptOwnerID,int exceptAppointmentID)
		{
		ResultSet rs = database.query("SELECT * FROM Appointment WHERE roomReservationID="+roomID+" AND startTime<'"+endTime+"' AND endTime>'"+startTime+"' AND isCancelled=0 AND NOT (ownerID="+exceptOwnerID+" AND appointmentID="+exceptAppointmentID+")");
		try
			{
			if(!rs.next())//If there is not such an appointment at that time(PS in that case there is only one)
				{
				return true;
				}
			rs.close();
			}
		catch (SQLException e)
			{e.printStackTrace();}
		return false;
		}
	//Clean shutdown!
	public void dispose()
		{
		System.out.println("Clean Shutdown");
		isRunning=false;
		
		for (TCPConnection c : tCPConnections)
			{
			c.closeSocket();
			}
		tCPConnections=null;
		
		try {myService.close();}
		catch (IOException e)
				{e.printStackTrace();}
		database.dispose();
		}

	//Connection accepter thread
	private class Accepter extends Thread
		{

		public void run()
			{
			while (isRunning)
				{
				try
					{
					TCPConnection c=new TCPConnection(myService.accept(),Server.this);
					tCPConnections.add(c);
					c.start();
					}
				catch (SocketException e)
					{System.out.println("Server Socket destroyed");}
				catch (IOException e)
					{e.printStackTrace();}
				}
			}
		}
	//Handle a login attempt
	public void login(String email, String password,TCPConnection source)
		{
		ResultSet rs = database.query("SELECT personID,password FROM Person WHERE email='"+email+"'");
		try
			{
			if(rs.next())
				{
				System.out.println("The correct password is "+rs.getString(1+1));
				if(password.equals(rs.getString(1+1)))
					{
					int personID = rs.getInt(1+0);
					if(!accesses.containsKey(personID))
						{
						source.setPersonID(personID);
						connectedPersons.put(personID, source);
						accesses.put(personID, new Access(personID));
						int nextAppointmentID = database.getNextAppointmentID(personID);
						source.send("<"+PROTOCOL.LOGIN_ACCEPT+'|'+personID+"|"+nextAppointmentID+">");
						//SEND LOTS OF STUFF
						ResultSet rs2;
						rs2 = database.getRooms();
						while (rs2.next())
							{
							source.sendRoom(rs2.getInt(1+0),rs2.getString(1+1),rs2.getInt(1+2));
							}
						rs2 = database.getDepartments();
						while (rs2.next())
							{
							source.sendDepartment(rs2.getInt(1+0),rs2.getString(1+1));
							}
						rs2 = database.getPersons();
						while (rs2.next())
							{
							source.sendPerson(rs2.getInt(1+0),rs2.getString(1+1),rs2.getString(1+3),rs2.getInt(1+4));
							}
						rs2 = database.getMessages(personID);
						while (rs2.next())
							{//if not deleted
							if(rs2.getInt(1+3)==0)
								{
								int auxField = -1;
								int type = rs2.getInt(1+2);
								if(type==TYPE.CHANGED)
									auxField = rs2.getInt(1+9);
								else if(type==TYPE.DENIED || type == TYPE.LEFT)
									auxField = rs2.getInt(1+6);
								source.sendMessage(type,rs2.getInt(1+0),rs2.getInt(1+1),rs2.getInt(1+7),rs2.getInt(1+8),auxField,rs2.getString(1+5),rs2.getTimestamp(1+4));
								}
							}
						System.out.println("User logged in");
						return;
						}
					}
				}
			rs.close();
			}
		catch (SQLException e)
			{
			e.printStackTrace();
			}
		System.out.println("Login failed");
		source.send("<"+PROTOCOL.LOGIN_ERROR+">");
		}
    }
